# session登录态判断处理
30 秒速记
session= 服务端存用户状态,浏览器Cookie里只放一个session id,每次请求带上它来认人- 有
Cookie不等于已登录:要拿id去存储里查到会话、没过期、里面有userId - 登录态校验写成中间件,通过后把用户放进
ctx.state.user,后面直接用 - 公开页、可选登录页、必须登录页分开挂,别全局一刀切
- 页面没登录 → 跳登录页;
API没登录 →401,登录了但没权限 →403
session 登录态的原理是:用户状态存在服务端,浏览器只拿一个 session id 放在 Cookie 里,每次请求服务端拿这个 id 去查是谁。 判断登录态时只看浏览器有没有这个 Cookie 是不够的,要真的去存储里查到会话,确认没过期、有 userId,比较严格的场景还要确认账号没被禁用。这段逻辑我会写成一个中间件,只挂在需要登录的路由上,通过后把用户信息放进 ctx.state.user;页面请求失败就跳登录页,接口请求失败就返回 401。
理解「session登录态判断处理」时,要先明确输入、运行阶段和可观察输出。这里的交互实验会分别执行正常路径与失败路径,并把 Node.js 的关键状态变化和同屏代码对应起来;不能只记命令名称。
工程中还要验证退出码、资源清理和异常输入。属于 security 场景的配置应保持最小可复现,并在开发环境与 CI 中使用相同命令复跑,避免把平台差异或隐式默认值带到线上。
HTTP 本身是无状态的,服务端分不清两次请求是不是同一个人发的。session 的思路是让服务端记账:用户登录成功后,服务端生成一个随机的 session id,把「这个 id 对应用户 42,已登录」存进内存、MySQL 或者 Redis,再通过 Set-Cookie 把 id 发给浏览器。之后浏览器每次请求自动带上这个 Cookie,服务端拿 id 一查就知道是谁。
一个能用的登录校验中间件大概长这样:
// middlewares/requireLogin.js
module.exports = function requireLogin({ api = false } = {}) {
return async (ctx, next) => {
const s = ctx.session
if (!s || !s.isLogin || !s.userId) {
if (api) {
ctx.status = 401
ctx.body = { success: false, code: 'NOT_LOGIN' }
} else {
ctx.redirect('/login?returnTo=' + encodeURIComponent(ctx.path))
}
return
}
ctx.state.user = { id: s.userId, name: s.userName }
await next()
}
}
// 只挂在需要登录的路由上
router.get('/work', requireLogin(), pageCtrl.work)
router.get('/user/getUserInfo.json', requireLogin({ api: true }), userCtrl.getLoginUserInfo)
几个容易忽略的点:session 中间件要先于这个中间件注册,否则 ctx.session 还没恢复;Cookie 要设 httpOnly,防止被页面脚本读走;returnTo 跳回时只允许站内路径,不然就是开放重定向漏洞。
面试官追问
追问 1只要请求带了 USER_SID 这个 Cookie 就认为已登录,有什么问题?
Cookie 是客户端随便能改的,随便填一个值也能带上。必须拿这个 id 去存储里查,查不到、过期了、里面没有 userId 都算没登录。
追问 2用户改了密码,其他设备上的旧登录要立刻失效,session 方案怎么做?
在用户表里加一个 tokenVersion,写 session 时一起存进去,校验中间件每次比对数据库里的版本,对不上就踢掉。或者改密码时直接把这个用户所有会话从存储里删掉,前提是存储能按 userId 查会话。
追问 3session 和 JWT 怎么选?
session 状态在服务端,踢人、改权限立刻生效,代价是要有共享存储;JWT 无状态、扩展方便,但签发出去就收不回来,只能靠短有效期加刷新令牌。传统网站、后台系统我一般用 session,开放给第三方或者跨很多服务的场景再考虑 JWT。
追问 4App 客户端存不住 Cookie,还能用这套 session 吗?
可以,把 session id 放进请求头比如 Authorization: Bearer <sid>,服务端从头里取再去存储查,逻辑不变。要注意头和 Cookie 两个来源别混着判断,同一个接口只认一种。
追问 5登录后一刷新就掉线,按什么顺序查?
先看登录响应有没有 Set-Cookie,再看刷新请求有没有带上这个 Cookie,最后看服务端拿这个 id 能不能查到会话。三步哪步断了就在哪查:没下发看中间件顺序,没带上看 Domain、Path、SameSite、Secure,查不到看存储。
# session登录态判断处理
# 使用session中间件
30 秒速记
koa-session-minimal提供ctx.session,koa-mysql-session把会话数据存进MySQLkey: 'USER_SID'是Cookie名,Cookie里只有session id,用户数据在MySQL里session中间件必须在所有读写ctx.session的中间件和路由之前注册- 多实例部署要共享存储(
MySQL/Redis),存内存的话请求打到另一台就掉线 Cookie记得配httpOnly、sameSite,HTTPS下加secure
这段配置做的事是:用 koa-session-minimal 给每个请求挂上 ctx.session,用 koa-mysql-session 把会话内容存到 MySQL,浏览器 Cookie 里只放一个叫 USER_SID 的 id。 存储放在 MySQL 而不是进程内存,是为了服务重启或者多台机器部署时登录态不丢。注册顺序很关键,session 中间件要放在路由前面,后面的代码才拿得到恢复好的 ctx.session。实际项目里 session 我更多放 Redis,读写快、自带过期,MySQL 存也能用,就是要定期清理过期记录。
// code ...
const session = require('koa-session-minimal')
const MysqlStore = require('koa-mysql-session')
const config = require('./../config')
// code ...
const app = new Koa()
// session存储配置
const sessionMysqlConfig= {
user: config.database.USERNAME,
password: config.database.PASSWORD,
database: config.database.DATABASE,
host: config.database.HOST,
}
// 配置session中间件
app.use(session({
key: 'USER_SID',
store: new MysqlStore(sessionMysqlConfig)
}))
// code ...
面试官追问
追问 1同事说这样配会把整个用户 session 写进 Cookie,对吗?
不对。配了 store 之后 Cookie 里只有 USER_SID=随机id,数据存在 MySQL 的会话表里。在 DevTools 的 Application 面板看 Cookie 值,再去库里按这个 id 查一下就清楚了。
追问 2session 中间件写在路由后面,会出现什么现象?
路由执行时 ctx.session 还不存在,读出来是 undefined,写进去也不会被保存,表现就是登录后永远是未登录。Koa 中间件按 app.use 的顺序执行,session 要放在 router.routes() 前面。
追问 3服务部署了三台,session 存在进程内存里,会怎样?
负载均衡把请求随机分到三台机器,登录时写在 A 机,下一个请求到了 B 机就查不到,用户随机掉线。要么用 MySQL / Redis 共享存储,要么在负载均衡上做会话粘滞,前者更靠谱。
追问 4数据库配置读不到时,想默认连本机 MySQL 继续启动,可以吗?
我不同意。会话可能写到一个错的库里,服务看着启动成功,用户却一直登不上,排查更费劲。关键配置缺失就让进程启动时直接报错退出,比带病运行好查得多。
追问 5线上所有用户间歇性被踢下线,日志里有 session 存储连接报错,先看什么?
先看 MySQL 连接数是不是打满了、有没有慢查询拖住连接池,会话表没清理过期数据也会越来越慢。确认后调连接池大小、给会话表加过期清理,长期看把 session 挪到 Redis
